Part1:创建和销毁对象

第1条:考虑用静态工厂方法代替构造器

第2条:遇到多个构造器参数时要考虑用构建器

第3条:用私有构造器或者枚举类型强化Singleton属性

声明一个私有构造方法覆盖默认的公有构造方法使得无法通过new来构造单例对象,而外部仅能通过一个公有的静态方法来获取单例类对象的引用。

第4条:通过私有构造器强化不可实例化的能力

有的类不希望被实例化,实例化对它没有意义(例如java.util.Collections、java.util.Math等工具类)。

为了避免误导用户不正当地实例化这些类,可以显式地定义一个私有构造器来保证该类不能被实例化。

另外,企图通过把类做成抽象类来强制该类不可实例化不是一个好主意。因为这会误导用户以为该类是专门为了继承而设计的,而且该类的子类可以被实例化。

 

第5条:避免创建不必要的对象

1.创建String对象避免使用:String s = new String("stringette"),因为“stringette”本身就是一个String实例,功能等同于构造器创建的对象,这种写法会创建出不必要的(重复的)String实例。

改进后的写法为:String s = "stringette",这种写法不是每次执行都创建一个新的实例,而且它能保证对于所有在同一台虚拟机中运行的代码,只要包含相同的字符串字面常量,该对象就会被重用。

2.对于同时提供静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器。构造器每次被调用都会创建一个新对象,而静态工厂方法不要求每次被调用都创建新对象,事实上也不会。

3.设计类的时,可以考虑将部分代码在static{}代码块中实现,这使得这部分代码只在该类初始化时运行一次,而不是每次调用类中某个函数时重复运行。

例如类中需要计算常量的情况,可以在static{}代码块中计算出常量的值,这样避免了每次调用某个需要常量的函数时重复计算常量的值。

4.要优先使用基本类型而不是包装类型,要当心无意识的自动装箱。例如:

1
2
3
4
5
Long sum = 0L; //如果把“Long”换成“long”,运行速度会变快很多
for(int i = 0; i < Integer.MAX_VALUE; i++){
sum += i; //隐式地自动装箱,创造了很多不必要的对象
}
System.out.println(sum);

5.本条标题“避免创建不必要的对象”不意味着创建对象的代价非常昂贵,相反,小对象的创建和回收动作是非常廉价的,特别是在现代JVM上。

因此,通过维护自己的对象池(object pool)来避免创建对象往往不是一个好的做法,除非对象池中的对象是非常重量级的。正确使用对象池的例子就是数据库连接池,因为建立数据库连接的代价是非常昂贵的(重量级的)。

 

第6条:消除过期的引用

例如:使用数组实现栈时,出栈时应当手动将出栈的元素的位置的引用赋为null,否则如果栈先增长后收缩,从栈中弹出的对象将不会被垃圾回收。

1
2
3
4
5
6
7
8
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
Object result = stack[--size];
stack[size] = null;
return result;
}

 

第7条:避免使用finalizer

1.为何要避免使用finalizer? (finalizer存在的问题)

​ <1>Java语言不仅不保证finalizer方法会被及时地执行,而且根本不保证它们会被执行。

​ <2>System.gc和System.runFinalization这两个方法同样不能保证finalizer方法一定会被执行。

​ <3>使用finalizer方法会产生一个严重的性能损失。

2.正确的表达“结束时做某件事”的方式是使用try-finally结构,在finally代码块中写入逻辑以确保及时执行。

3.其它注意点:终结方法链(finalizer chaining)并不会被自动执行。也即如果有子类覆盖了父类的终结方法,子类的终结方法就必须手工调用父类的终结方法。

 

Part2:通用方法

第8条:覆盖equals时遵守通用约定

1.equals用于表达“逻辑相等”概念的语义。

2.equals实现了等价关系(自反性、对称性、传递性、一致性)。一致性意为多次调用结果相同。

3.注意不要将equals声明中的Object对象替换为其它类型,比如:

public boolean equals(MyClass o){}

由于Java中Object类中的equals方法的形参是Object类型,上述方法不能实现覆盖的愿望,而是进行了一次重载(Overload),在原equals的基础上再提供一个强类型的equals方法。

4.equals的优良实现:

​ <1>使用==操作符检查是否为同一对象的引用,若是则直接true

​ <2>使用instanceof操作符检查是否为相同的类型,若不是则直接false

​ <3>把参数转换成正确的类型

​ <4>根据业务逻辑对“相等”的需求,逐域检查是否相等

​ <5>检验是否满足:对称性、传递性、一致性

​ <6>覆盖equals时总是要覆盖hashCode

 

第9条:覆盖equals时总是要覆盖hashCode

1.如果两个对象根据equals方法比较是相等的,那么hashCode也必须产生相同整数结果。这是Object.hashCode的通用约定。

2.如果一个类是不可变的,并且计算hashCode的开销较大,就应该考虑把hashCode缓存在对象内部,而不是每次请求的时候都重新计算hashCode。

 

第10条:始终要覆盖toString

1.当对象被传递给println、printf、字符串联操作符(+)一级assert或者被调试器打印出来时,toString方法会被自动调用。

2.提供好的toString实现可以使类用起来更加舒适。

 

第11条:谨慎地覆盖clone

第12条:考虑实现Comparable接口

Part3:类和接口

第13条:使类和成员的可访问性最小化

封装是软件设计的基本原则之一。这使得模块可以独立地开发、测试、优化、使用、理解、修改,从而降低构建大型系统的风险。

1.对于非嵌套的类和接口(不是内部类):如果是包内的实现,则应该把该类(接口)做成包级私有,他实际上成为包的实现的一部分,而不是该包导出的API。相反,如果要向外提供API则将该类(接口)做成公有的。

2.对于成员(域、方法、嵌套类、嵌套接口):访问权限最小化。

3.有一条规则限制了降低方法可访问性:如果方法覆盖了父类中的一个方法,子类中的访问级别就不允许低于父类中的访问级别,这样可以确保任何可使用父类的实例的地方也都可以使用子类的实例。

 

第14条:在公有类中使用访问方法而非公有域

对于可变类再来说,公有域总是应该被私有域和访问方法(getter、setter)代替。

 

第15条:使可变性最小

第16条:复合优先于继承

对于父类和子类的实现都处于同一个程序员的控制之下的情形,继承是比较安全的。然而对于没有文档说明的“外来”类进行子类化是非常危险的。

如果对于扩展类功能的需求不通过继承现有的类的方式,而是在行动类中增加一个私有域,它引用现有类第一个实例,这种设计被称作复合(composition),现有类变成了新类的一个组件。新类中的方法被称为转发方法

1.使用继承扩展类的功能可能导致传递API中的缺陷,也可能导致由于不清楚父类方法实现的细节从而错误。因为后者而引发错误经典例子是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
意图扩展HashSet类,使之支持查询从创建以来曾经添加了多少个元素的功能。
*/
public class InstrumentedHashSet<E> extends HashSet<E>{
private int addCount = 0;
public InstrumentedHashSet(){}
public InstrumentedHashSet(int initCap, float loadFactor){}
@Override public boolean add(E e){
addCount++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c){
addCount += c.size();
return super.addAll(c);//其实是将逐个add集合c中的元素
}
public int getAddCount(){
return addCount;
}
}
1
2
3
4
5
6
7
8
9
10
/*
java.util AbstractSet<E>中的addAll方法,子类HashSet<E>中的addAll方法中使用了本类的addAll方法
*/
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}

这个类看上去合理实际则不能正确工作。原因HashSet中addAll方法的具体实现最终也是调用add方法,也即如果元素是通过addAll方法加入到InstrumentedHashSet中的,那么addCount就多算了一倍。

因此,因为不清楚父类的具体实现时,通过继承来实现扩展功能的风险非常大。

2.如果想要扩展类的功能,使用复合优于使用继承。而只有当子类真正是超类的子类型时,才适合用继承。或者说对于两个类A和B,只有当两者之间确实存在“is-a”关系的时候,类B才应该继承类A。

3.在决定使用继承而不是复合之前,应该问自己一组问题。对于你正在试图扩展的类,它的API中有没有缺陷呢?如果有,你是否愿意把这些缺陷传播到类的API中?继承机制会把超类API中的所有缺点传播到子类中,而复合则允许设计新的API来隐藏这些缺陷。

第17条:要么为继承而设计,并提供文档说明,要么就禁止继承

第16条中已经描述了继承存在的隐患,例如由于父类可能存在这样的情况:A方法的实现调用了B方法,子类覆盖了(子类重写父类的方法,要求方法名和参数类型完全一样)B方法,最终导致A方法工作不正常。

因此,当为了继承而设计类的时候,为了防止子类的设计者错误地扩展父类,父类的设计者需要有选择的暴露一些受保护的方法或者域,且在能达成目标的情况下尽可能少地暴露信息。如果没有文档说明,那么久应该禁止继承。

为了允许继承,类还不洗遵守其他一些约束:构造器绝不能调用可被覆盖的方法,无论是直接调用还是间接调用

此外,如果在一个为了继承而设计的类中实现Cloneable或Serializable接口,就应该意识到clone方法和readObject方法在行为上非常类似于构造器,因此类似的限制规则也是适用的:clone方法和readObject方法中不能调用可覆盖的方法。

第18条:接口优于抽象类

接口优于抽象类的理由:

1.现有的类可以很容易地被更新以实现新的接口,而一般来说无法更新现有的类来扩展新的抽象类。例如Comparable接口引入Java平台时,很多现有的类更新实现了Comparable接口,而如果要让它们继承某个抽象类,则会破坏原有的类层次结构,是不现实的。

2.接口是mixin(混合类型)的理想选择。继承表达的关系是两个类A和B中存在“A is B”的关系,而接口表达的语义是“A can do XX”。而现实中也存在很多情况会符和“A is not B, but both A and B can do C”,这时应该通过接口的方式定义行为C而不是通过抽象类。

[注:mixin是指这样的类型:类除了实现它的“基本类型”之外,还可以实现mixin类型,表明它提供了某些可供选择的行为。例如,Comparable就是一个mixin接口,它允许类表明它的实例可以与其他可相互比较的对象进行排序。]

3.接口允许我们构造非层次结构的类型框架。(未展开)

 

第19条:接口只用于定义类型

类实现接口时,接口就充当可以引用这个类的实例的类型,就表明客户端可以对这个类的实例实施某些动作。这是接口定义类型。

而如果接口中不含任何方法,而只包含静态final域,每一个域都导出一个常量,使用这些常量的类实现该接口以避免用类名来修饰常量名,这是接口用来导出常量。

接口应该只被用来定义类型,不应该被用来导出常量。

[注:如果需要导出常量,另一种可行的方法是使用不可实例化的工具类:将一个类的默认构造器写成私有使之不可实例化,在类中定义public static final修饰的字段则可以导出常量]

 

第20条:类层次优于标签类

(不知道什么是标签类?那就不知道吧,反正它不好…)

第21条:用函数对象表示策略

第22条:优先考虑静态成员类

Part4:泛型

第23条:请不要在新代码中使用原生态类型

第24条:消除非受检警告

第25条:列表由于数组

第26条:优先考虑泛型

第27条:优先考虑泛型方法

第28条:利用有限制通配符来提升API的灵活性

第29条:优先考虑类型安全的异构造器

Part5:枚举和注解

第30条:用enum代替int常量

第31条:用实例域代替序数

第32条:用EnumSet代替位域

第33条:用EnumMap代替序数索引

第34条:用接口模拟可伸缩的枚举

第35条:注解由于命名模式

第36条:坚持使用Override注解

第37条:用标记接口定义类型

Part6:方法

第38条:检查参数的有效性

第39条:必要时进行保护性拷贝

第40条:谨慎设计方法签名